# -*- coding: utf-8 -*-
# FileName:     RaidSim.py
# time:         22/12/4 004 下午 3:25
# Author:       Zhou Hang
# Description:  基于OSTep RaidSim.py修改的RAID模拟器


import random
from optparse import OptionParser

from common.zerror import zerror
from common.zutils import convert

# minimum unit of transfer to RAID
BLOCKSIZE = 4096


class disk:
    def __init__(self, seekTime=10, xferTime=0.1, queueLen=8):
        # 单位都是ms
        # 寻道时间，数据传输时间，
        self.seekTime = seekTime
        self.xferTime = xferTime

        # length of scheduling queue
        self.queueLen = queueLen

        # current location: make it negative so that whatever
        # the first read is, it causes a seek
        self.currAddr = -10000

        # queue
        self.queue = []

        # DiskSim geometry
        self.numTracks = 100
        self.blocksPerTrack = 100
        self.blocksPerDisk = self.numTracks * self.blocksPerTrack

        # stats
        self.countIO = 0
        self.countSeq = 0
        self.countNseq = 0
        self.countRand = 0
        self.utilTime = 0

    def stats(self):
        return self.countIO, self.countSeq, self.countNseq, self.countRand, self.utilTime

    def enqueue(self, addr):
        assert (addr < self.blocksPerDisk)
        self.countIO += 1

        # check if this is on the same track, or a different one
        currTrack = self.currAddr / self.numTracks
        newTrack = addr / self.numTracks

        # absolute diff
        diff = addr - self.currAddr
        if diff < 0:
            diff = -diff

        # if on the same track...
        if currTrack == newTrack or diff < self.blocksPerTrack:
            if diff == 1:
                self.countSeq += 1
            else:
                self.countNseq += 1
            self.utilTime += (diff * self.xferTime)
        else:
            self.countRand += 1
            self.utilTime += (self.seekTime + self.xferTime)
        self.currAddr = addr

    def go(self):
        return self.utilTime


class raid:
    def __init__(self, chunkSize='4k', numDisks=4, level=0, timing=False, reverse=False, solve=False, raid5type='LS'):
        self.printPhysical = None
        chunkSize = int(convert(chunkSize))
        self.chunkSize = chunkSize // BLOCKSIZE
        self.numDisks = numDisks
        self.raidLevel = level
        self.timing = timing
        self.reverse = reverse
        self.solve = solve
        self.raid5type = raid5type

        if (chunkSize % BLOCKSIZE) != 0:
            zerror(f'块大小({chunkSize})必须是blocksize({BLOCKSIZE})的倍数:({self.chunkSize % BLOCKSIZE})')
        if self.raidLevel == 1 and numDisks % 2 != 0:
            zerror(f'raid1: 磁盘数量({numDisks})必须是2的倍数')

        if self.raidLevel == 4:
            self.blocksInStripe = (self.numDisks - 1) * self.chunkSize
            self.pdisk = self.numDisks - 1
        if self.raidLevel == 5:
            self.blocksInStripe = (self.numDisks - 1) * self.chunkSize
            self.pdisk = -1

        self.disks = []
        for i in range(self.numDisks):
            self.disks.append(disk())

    # print per-DiskSim stats
    def stats(self, totalTime):
        for d in range(self.numDisks):
            s = self.disks[d].stats()
            if totalTime > 0.0:
                util = (100.0 * float(s[4]) / totalTime)
            else:
                util = 0.0

            # 这里是什么情况？
            if s[4] == totalTime:
                print(f'磁盘{d}- 占用率: {util:3.2f}  I/Os: {s[0]:5d} (顺序次数:{s[1]} 同一个磁道:{s[2]} 随机次数:{s[3]})')
            elif s[4] == 0:
                print(f'磁盘{d}- 占用率: {util:3.2f}  I/Os: {s[0]:5d} (顺序次数:{s[1]} 同一个磁道:{s[2]} 随机次数:{s[3]})')
            else:
                print(f'磁盘{d}- 占用率: {util:3.2f}  I/Os: {s[0]:5d} (顺序次数:{s[1]} 同一个磁道:{s[2]} 随机次数:{s[3]})')

    # global enqueue function
    def enqueue(self, addr, size, isWrite):
        # should we print out the logical operation?
        if not self.timing:
            if self.solve or not self.reverse:
                if isWrite:
                    print(f'逻辑 写入地址: {addr} 大小:{size * BLOCKSIZE}')
                else:
                    print(f'逻辑 读 地址: {addr} 大小: {size * BLOCKSIZE}')
                if not self.solve:
                    print('  物理 读/写?\n')
            else:
                print('逻辑操作是?')

        # should we print out the physical operations?
        if not self.timing and (self.solve or self.reverse == True):
            self.printPhysical = True
        else:
            self.printPhysical = False

        if self.raidLevel == 0:
            self.enqueue0(addr, size, isWrite)
        elif self.raidLevel == 1:
            self.enqueue1(addr, size, isWrite)
        elif self.raidLevel == 4 or self.raidLevel == 5:
            self.enqueue45(addr, size, isWrite)

    # ProcSims DiskSim workloads one at a time, returning final completion time
    def go(self):
        tmax = 0
        for d in range(self.numDisks):
            t = self.disks[d].go()
            if t > tmax:
                tmax = t
        return tmax

    # helper functions
    def doSingleRead(self, disk, off, doNewline=False):
        if self.printPhysical:
            print(f'  读  [磁盘 {disk}, 偏移 {off}]  ')
            if doNewline:
                print('')
        self.disks[disk].enqueue(off)

    def doSingleWrite(self, disk, off, doNewline=False):
        if self.printPhysical:
            print(f'  写 [磁盘 {disk}, 偏移 {off}]  ')
            if doNewline:
                print('')
        self.disks[disk].enqueue(off)

    #
    # mapping for RAID 0 (striping)
    #
    def bmap0(self, bnum):
        cnum = bnum // self.chunkSize
        coff = bnum % self.chunkSize
        return cnum % self.numDisks, (cnum // self.numDisks) * self.chunkSize + coff

    def enqueue0(self, addr, size, isWrite):
        # can ignore isWrite, as I/O pattern is the same for striping
        for b in range(addr, addr + size):
            (disk, off) = self.bmap0(b)
            if isWrite:
                self.doSingleWrite(disk, off, True)
            else:
                self.doSingleRead(disk, off, True)
        if self.timing == False and self.printPhysical:
            print('')

    #
    # mapping for RAID 1 (mirroring)
    #
    def bmap1(self, bnum):
        cnum = bnum // self.chunkSize
        coff = bnum % self.chunkSize
        disk = 2 * (cnum % (self.numDisks // 2))
        return disk, disk + 1, (cnum // (self.numDisks // 2)) * self.chunkSize + coff

    def enqueue1(self, addr, size, isWrite):
        for b in range(addr, addr + size):
            (disk1, disk2, off) = self.bmap1(b)
            # print 'enqueue:', addr, size, '-->', m
            if isWrite:
                self.doSingleWrite(disk1, off, False)
                self.doSingleWrite(disk2, off, True)
            else:
                # the RaidSim-1 read balancing algorithm is here;
                # could be something more intelligent --
                # instead, it is just based on the DiskSim offset
                # to produce something easily reproducible
                if off % 2 == 0:
                    self.doSingleRead(disk1, off, True)
                else:
                    self.doSingleRead(disk2, off, True)
        if not self.timing and self.printPhysical:
            print('')

    #
    # mapping for RAID 4 (parity DiskSim)
    #
    # assumes (for now) that there is just one parity DiskSim
    #
    def bmap4(self, bnum):
        cnum = bnum // self.chunkSize
        coff = bnum % self.chunkSize
        return cnum % (self.numDisks - 1), (cnum // (self.numDisks - 1)) * self.chunkSize + coff

    def pmap4(self, snum):
        return self.pdisk

    #
    # mapping for RAID 5 (rotated parity)
    #
    def __bmap5(self, bnum):
        cnum = bnum // self.chunkSize
        coff = bnum % self.chunkSize
        ddsk = cnum // (self.numDisks - 1)
        doff = (ddsk * self.chunkSize) + coff
        disk = cnum % (self.numDisks - 1)
        col = (ddsk % self.numDisks)
        pdisk = (self.numDisks - 1) - col

        # supports left-asymmetric and left-symmetric layouts
        if self.raid5type == 'LA':
            if disk >= pdisk:
                disk += 1
        elif self.raid5type == 'LS':
            disk = (disk - col) % self.numDisks
        else:
            zerror('error: no such RAID scheme')
        assert (disk != pdisk)
        return disk, pdisk, doff

    # yes this is lame (redundant call to __bmap5 is serious programmer laziness)
    def bmap5(self, bnum):
        (disk, pdisk, off) = self.__bmap5(bnum)
        return disk, off

    # this too is lame (redundant call to __bmap5 is serious programmer laziness)
    def pmap5(self, snum):
        (disk, pdisk, off) = self.__bmap5(snum * self.blocksInStripe)
        return pdisk

    # RAID 4/5 helper routine to write out some blocks in a stripe
    def doPartialWrite(self, stripe, begin, end, bmap, pmap):
        numWrites = end - begin
        pdisk = pmap(stripe)
        if (numWrites + 1) <= (self.blocksInStripe - numWrites):
            # SUBTRACTIVE PARITY
            # print 'SUBTRACTIVE'
            offList = []
            for voff in range(begin, end):
                (disk, off) = bmap(voff)
                self.doSingleRead(disk, off)
                if off not in offList:
                    offList.append(off)
            for i in range(len(offList)):
                self.doSingleRead(pdisk, offList[i], i == (len(offList) - 1))
        else:
            # ADDITIVE PARITY
            # print 'ADDITIVE'
            stripeBegin = stripe * self.blocksInStripe
            stripeEnd = stripeBegin + self.blocksInStripe
            for voff in range(stripeBegin, begin):
                (disk, off) = bmap(voff)
                self.doSingleRead(disk, off, (voff == (begin - 1)) and (end == stripeEnd))
            for voff in range(end, stripeEnd):
                (disk, off) = bmap(voff)
                self.doSingleRead(disk, off, voff == (stripeEnd - 1))

        # WRITES: same for additive or subtractive parity
        offList = []
        for voff in range(begin, end):
            (disk, off) = bmap(voff)
            self.doSingleWrite(disk, off)
            if off not in offList:
                offList.append(off)
        for i in range(len(offList)):
            self.doSingleWrite(pdisk, offList[i], i == (len(offList) - 1))

    # RAID 4/5 enqueue routine
    def enqueue45(self, addr, size, isWrite):
        if self.raidLevel == 4:
            (bmap, pmap) = (self.bmap4, self.pmap4)
        elif self.raidLevel == 5:
            (bmap, pmap) = (self.bmap5, self.pmap5)

        if not isWrite:
            for b in range(addr, addr + size):
                (disk, off) = bmap(b)
                self.doSingleRead(disk, off)
        else:
            # ProcSims the write request, one stripe at a time
            initStripe = addr // self.blocksInStripe
            finalStripe = (addr + size - 1) // self.blocksInStripe

            left = size
            begin = addr
            for stripe in range(initStripe, finalStripe + 1):
                endOfStripe = (stripe * self.blocksInStripe) + self.blocksInStripe

                if left >= self.blocksInStripe:
                    end = begin + self.blocksInStripe
                else:
                    end = begin + left

                if end >= endOfStripe:
                    end = endOfStripe

                self.doPartialWrite(stripe, begin, end, bmap, pmap)

                left -= (end - begin)
                begin = end

        # for all cases, print this for pretty-ness in mapping mode
        if self.timing == False and self.printPhysical:
            print('')


class option:
    """
    TODO:在qt中可以不使用OptionParser来处理参数
    """
    def __init__(self):
        pass


class core:
    def __init__(self, seed=0, numDisks=4, chunkSize='4k', numRequests=10, reqSize='4k', workload='rand',
                 writeFrac=50, randRange=20, level=0, raid5='LS', timing=False):
        self.workloadIsSequential = None
        self.size = None
        self.writeFrac = None
        self.option_parser(seed, numDisks, chunkSize, numRequests, reqSize,
                           workload, writeFrac, randRange, level, raid5, timing)
        # self.show_option()
        self.check_args()

        options = self.options  # 这里仅仅用于改个名
        self.r = raid(chunkSize=options.chunkSize, numDisks=options.numDisks, level=options.level,
                      timing=options.timing,
                      reverse=options.reverse, solve=options.solve, raid5type=options.raid5type)

        self.gen_requests()

    def option_parser(self, seed=0, numDisks=4, chunkSize='4k', numRequests=10, reqSize='4k', workload='rand',
                      writeFrac=0, randRange=20, level=0, raid5='LS', timing=False):
        parser = OptionParser()
        parser.add_option('-s', '--seed', default=seed, help='随机种子 默认0', action='store', type='int', dest='seed')
        parser.add_option('-D', '--numDisks', default=numDisks, help='RAID中磁盘数量 默认4', action='store', type='int',
                          dest='numDisks')
        parser.add_option('-C', '--chunkSize', default=chunkSize, help='块大小 默认4KB', action='store', type='string',
                          dest='chunkSize')
        parser.add_option('-n', '--numRequests', default=numRequests, help='模拟请求的数量 默认10', action='store',
                          type='int', dest='numRequests')
        parser.add_option('-S', '--reqSize', default=reqSize, help='每次请求大小，默认4K', action='store', type='string',
                          dest='size')
        parser.add_option('-W', '--workload', default=workload, help='工作负载rand(随机)或seq(顺序) 默认rand',
                          action='store',
                          type='string', dest='workload')
        parser.add_option('-w', '--writeFrac', default=writeFrac, help='请求中写的比例一个0-100的值 默认0(全都是读请求)',
                          action='store', type='int', dest='writeFrac')
        parser.add_option('-R', '--randRange', default=randRange, help='rand工作负载下请求的范围 默认10000',
                          action='store', type='int', dest='range')
        parser.add_option('-L', '--level', default=level, help='RAID级别(支持0, 1, 4, 5) 默认0', action='store', type='int',
                          dest='level')
        parser.add_option('-5', '--raid5', default=raid5,
                          help='RAID-5 left-symmetric左对称 "LS" or left-asym左不对称 "LA"',
                          action='store', type='string', dest='raid5type')
        parser.add_option('-r', '--reverse', default=False, help='instead of showing logical ops, show physical',
                          action='store_true', dest='reverse')
        parser.add_option('-t', '--timing', default=timing, help='使用timing模式',
                          action='store_true',
                          dest='timing')
        parser.add_option('-c', '--compute', default=True, help='计算答案', action='store_true',
                          dest='solve')

        (self.options, self.args) = parser.parse_args()

    def show_option(self):
        print('ARG blockSize', BLOCKSIZE)
        print('ARG seed', self.options.seed)
        print('ARG numDisks', self.options.numDisks)
        print('ARG chunkSize', self.options.chunkSize)
        print('ARG numRequests', self.options.numRequests)
        print('ARG reqSize', self.options.size)
        print('ARG workload', self.options.workload)
        print('ARG writeFrac', self.options.writeFrac)
        print('ARG randRange', self.options.range)
        print('ARG level', self.options.level)
        print('ARG raid5', self.options.raid5type)
        print('ARG reverse', self.options.reverse)
        print('ARG timing', self.options.timing)

    def check_args(self):
        self.writeFrac = float(self.options.writeFrac) / 100.0
        assert (0.0 <= self.writeFrac <= 1.0)

        random.seed(self.options.seed)

        self.size = convert(self.options.size)
        if self.size % BLOCKSIZE != 0:
            zerror(f'需要的容量({self.size}) 必须是 BLOCKSIZE({BLOCKSIZE}) 的倍数')
        self.size //= BLOCKSIZE

        if self.options.workload == 'seq' or self.options.workload == 's' or self.options.workload == 'sequential':
            self.workloadIsSequential = True
        elif self.options.workload == 'rand' or self.options.workload == 'r' or self.options.workload == 'random':
            self.workloadIsSequential = False
        else:
            zerror('工作负载必须是 r/rand/random 或 s/seq/sequential')

        assert (self.options.level in [0, 1, 4, 5])
        if self.options.level != 0 and self.options.numDisks < 2:
            zerror(f'RAID-4 和 RAID-5 需要 1 个以上的磁盘，你的输入为{self.options.level}')

        if self.options.level == 5 and self.options.raid5type != 'LA' and self.options.raid5type != 'LS':
            zerror(f'仅支持两种 RAID-5: left-asymmetric (LA) and left-symmetric (LS) (不支持{self.options.raid5type})')

    def gen_requests(self):
        # 生成请求
        off = 0
        for i in range(self.options.numRequests):
            if self.workloadIsSequential:
                blk = off
                off += self.size
            else:
                blk = int(random.random() * self.options.range)
            if random.random() < self.writeFrac:
                self.r.enqueue(blk, self.size, True)
            else:
                self.r.enqueue(blk, self.size, False)

        # 处理请求
        t = self.r.go()

        # 打印一些最终数据
        if not self.options.timing:
            return

        if self.options.solve:
            self.r.stats(t)
            print(f'总时间:{t}ms')
        else:
            print('Estimate how long the workload should take to complete.')
            print('- Roughly how many requests should each DiskSim receive?')
            print('- How many requests are random, how many sequential?')


def main():
    core()


if __name__ == "__main__":
    main()
